Skip to content

feat/bug: replace offset histogram with phrase-performance summary#1514

Open
XaiaX wants to merge 1 commit into
YARC-Official:devfrom
XaiaX:feat/vocals-summary
Open

feat/bug: replace offset histogram with phrase-performance summary#1514
XaiaX wants to merge 1 commit into
YARC-Official:devfrom
XaiaX:feat/vocals-summary

Conversation

@XaiaX

@XaiaX XaiaX commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Summary

The score screen's Advanced view shows a per-note hit-offset histogram (ms early/late) for every instrument — including vocals, where it's not applicable: vocals are graded per phrase on a qualitative scale, not per discrete note with a timing offset. This PR removes the offset histogram for vocals and replaces it with a vocals-appropriate phrase-performance summary:

  1. Phrase Breakdown histogram — one bar per vocal phrase, left→right in song order. Bar
    height is the phrase's normalized score; Awesome phrases render in a gold gradient, everything
    else in alternating grays. Tinted background regions and subtle boundary lines mark the grade
    tiers (Awful/Messy/Okay/Good/Strong/Awesome — the same tiers as the in-game HUD popup).
  2. Ranked tally — a two-column table counting phrases per tier, best→worst. All six tiers
    always show (even at zero) so multiple vocalists' cards line up row-for-row.
  3. Percussion rowhits / total for vocal percussion, shown only when the chart has any;
    the numerator goes gold on 100%.

No Core changes / replay-version bump

The engine already raises OnPhraseHit with the normalized phrase percent, and VocalsPlayer subscribes to it. The per-phrase scores are captured Unity-side as they fire and routed to the score screen through PlayerScoreCard (GameManagerScoreScreenMenuVocalsScoreCard). Nothing is stored in YARG.Core or serialized into replays.

The per-phrase list is not persisted in the replay file. The score screen is only reached via a live engine run — either playing normally or via "play with replay." In both cases the engine fires OnPhraseHit as it plays, populating the summary with no migration. The replay viewer (watch-only) does not reach the score screen.

The captures are plain Unity-side lists, not part of the engine state that BaseStats.Reset() clears. SetReplayTime and ResetPracticeSection both re-fire OnPhraseHit and OnNoteHit from scratch, so both call ResetScoreScreenCaptures() before the engine reprocesses, preventing the re-fired events from appending duplicates. (BaseStats.OffsetSamples doesn't need this because BaseStats.Reset() already empties it.)

Implementation notes

  • Single source of truth for grade thresholds: new Gameplay/Vocals/VocalPhraseGrade.cs holds the tier enum and lower bounds (0.0 / 0.1 / 0.6 / 0.7 / 0.8 / 1.0). These are the same values that were previously hardcoded in VocalsPlayerHUD.GetVocalPerformanceText's inline switch; that method now delegates to VocalPhraseGrade.Classify, and the histogram's tier regions/lines read LowerBound(), so the HUD popup and the score screen cannot drift apart. The HUD is a separate consumer — the thresholds live in VocalPhraseGrade, not in the HUD, so changing them means editing one file. The change to the HUD is not purely cosmetic: the old switch compared a double against float literals (>= 0.6f is really
    >= 0.60000002…), so a phrase at exactly 0.6 graded Messy; Classify uses doubles throughout, so it now grades Okay. Affects only values landing exactly on a threshold; arguably a fix.
  • ScoreCard hooks for subclass customization: ScoreCard gains protected virtual bool ShouldShowOffsetHistogram => true;VocalsScoreCard overrides it to false, hiding the offset histogram. The base card also exposes AdvancedStatsRect, AdvancedAccentColor, and a card-styled CreateStatLabel(...) factory for subclasses to build their own advanced content. Nothing global is mutated.
  • Rendering: new static VocalsPhraseHistogram.Build(...) constructs UGUI Image/RawImage bars, TMP labels, and layout groups at runtime — no prefab changes needed. It builds inside the card's AdvancedStatsRect, uses AdvancedAccentColor for the tally divider, and creates labels via the card's CreateStatLabel(...) factory so they inherit the prefab's text styling. It mirrors the offset histogram's footprint (132px graph height, 54px horizontal margins) and resolves fonts by name from already-loaded assets.
  • Percussion tally: this is new tracking since percussion hits weren't surfaced before. VocalsStats doesn't track percussion, so hits are counted from engine OnNoteHit for note.IsPercussion. The total (denominator) is computed once from the chart data at init (NoteTrack.Notes.Sum(phrase => phrase.ChildNotes.Count(n => n.IsPercussion))) rather than accumulated from engine callbacks. This was originally done to work around the shared-mutable-state bug, but is independently correct and will work after a fix. (The bug: CloneAsInstrumentDifficulty copies VocalNote references, so multiple solo-vocals engines on the same VocalsPart share the same VocalNote objects. The first engine to hit a percussion note mutates WasHit on the shared object, causing sibling engines to skip that note. With callback-counted totals, each player saw a different denominator. Chart-derived total is authoritative and identical for all players. The shared-state issue (only one player can actually hit a given percussion note) remains and is in a discord thread, already.
  • Localization: two new keys (Menu.ScoreScreen.PhraseSummaryHeader, …​.Percussion) in en-US.json; tier labels reuse the existing Gameplay.Vocals.Performance.<Tier> keys.

Files touched

File Change
Gameplay/Player/VocalsPlayer.cs Capture phrase percents + percussion hits; percussion total from chart data; clear on replay seek / practice reset.
Gameplay/GameManager.cs Route the vocals fields into PlayerScoreCard.
Gameplay/HUD/VocalsPlayerHUD.cs Grade text now delegates to VocalPhraseGrade.Classify.
Gameplay/Vocals/VocalPhraseGrade.cs (new) Tier enum + thresholds + Classify/LowerBound.
Menu/ScoreScreen/ScoreScreenContainer.cs New PlayerScoreCard fields.
Menu/ScoreScreen/ScoreScreenMenu.cs Hand the vocals data to VocalsScoreCard.
Menu/ScoreScreen/ScoreCards/ScoreCard.cs ShouldShowOffsetHistogram hook + advanced accessors.
Menu/ScoreScreen/ScoreCards/VocalsScoreCard.cs Hide offset histogram; forward data to phrase summary.
Menu/ScoreScreen/ScoreCards/VocalsPhraseHistogram.cs (new) Histogram + tally + percussion renderer.
StreamingAssets/lang/en-US.json Two new keys.

Behavior changes beyond the score screen

  • Exact tier boundaries grade slightly differently in the HUD popup (see "Single source of truth" above for the full explanation). Affects only values landing exactly on a threshold.

Caveats

  • Seven small Texture2Ds are cached in static fields and never destroyed (five 1×256 tier region gradient fills, two 1×256 bar gradients, ~1 KB each). They're created once and reused across score screens for the app lifetime — a deliberate, bounded ~7 KB tradeoff, not a growing leak. The GetOrCreate* null checks use Unity's overloaded == null, so destroyed textures (e.g. editor play-mode cycles with domain reload off) are recreated correctly.
  • Font resolution is by name (RedHatDisplay-ExtraBold, Barlow-Medium) via Resources.FindObjectsOfTypeAll<TMP_FontAsset>(), with fallback to the label factory's font if not found. Avoids serialized prefab references at the cost of a string match. Cached references are null-checked (not flag-guarded) so destroyed fonts (e.g. domain reload with Enter Play Mode Options) are re-resolved automatically — same pattern as the texture caches above.
  • The per-phrase data is not in the replay file (see above) Anything that wanted to show phrase scores without re-running the engine (e.g. a future replay-browser preview) would need Core serialization.
  • No automated tests — the Unity project has no test assemblies; verification was manual (see below). YARG.Core is untouched so no new tests, there.

Testing done

  • Played vocals charts to the score screen: offset histogram gone, one bar per phrase in song order, tier regions/lines correct, full six-tier tally, percussion row present only when the chart has percussion and gold numerator on 100%. Also tested with multiple simultaneous vocalists with no observed issues.
  • Harmonies: one card per vocalist with independent phrase lists, used a chart with different number of phrases per part to verify no issues seen.
  • Replays: "play with replay" advanced to the score screen with correct data, including pausing just after a percussion note to make sure it didn't double-count it. Seeking in a replay view didn't over-increment score on repeated percussion hits.
  • Non-vocals instruments: offset histogram unchanged.

Replaces the non-applicable hit-offset histogram on vocals score cards with a
vocals-appropriate phrase-performance summary: a per-phrase bar histogram
with tier-colored background regions, a ranked tier tally, and a percussion
hits/total row.

@polson polson left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested, code and functionality looks good to me.

Just one change for you to make

{
// The hit-offset histogram is meaningless for vocals (graded per phrase, not per note);
// we render a phrase summary in its place instead.
protected override bool ShouldShowOffsetHistogram => false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a template method would be a better approach for this:

   // Base
   protected virtual void BuildAdvancedContent()
   {
       BuildOffsetHistogram();
   }

   // VocalsScoreCard
   protected override void BuildAdvancedContent()
   {
       VocalsPhraseHistogram.Build(...);
   }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants